Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf(types): replace intersection with union to get better perf #3443

Merged
merged 5 commits into from
Oct 11, 2024

Conversation

m-shaka
Copy link
Contributor

@m-shaka m-shaka commented Sep 23, 2024

The author should do the following, if applicable

  • Add tests
  • Run tests
  • bun run format:fix && bun run lint:fix to format the code
  • Add TSDoc/JSDoc to document the code

details

This leads a breaking change

I found that massive amount of intersection increase type instantiation.
So, I've tried replacing intersections to union and as a result, this has decreased type instantiation drastically, that is, by about 70%.
image

However, union here is not intuitive and this may be a breaking change for users manipulating type of Hono somehow. I'm not certain how they do that, though

@yusukebe What's your thought on that? Do you think we should go for it this way?

@m-shaka m-shaka force-pushed the replace-intersection-with-union branch from 06610f8 to 60bffbb Compare September 23, 2024 07:12
Copy link

codecov bot commented Sep 23, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 95.58%. Comparing base (f1ec415) to head (e97c2e0).
Report is 7 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #3443   +/-   ##
=======================================
  Coverage   95.58%   95.58%           
=======================================
  Files         155      155           
  Lines        9357     9357           
  Branches     2746     2784   +38     
=======================================
  Hits         8944     8944           
  Misses        413      413           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@yusukebe
Copy link
Member

Hi @m-shaka

So, I've tried replacing intersections to union and as a result, this has decreased type instantiation drastically, that is, by about 70%.

Awesome!

However, union here is not intuitive and this may be a breaking change for users manipulating type of Hono somehow. I'm not certain how they do that, though

I've been thinking about this for a while: We can care less about changing the type definitions of handlers. A few users customize them, but we don't have to care about them though we have to take care of actual runtime behaviors. These are at risk own. So, it's okay to go with this.

Plus, regarding the types, we must care about the performance, most importantly.

Shall we go ahead with this PR?

@m-shaka
Copy link
Contributor Author

m-shaka commented Sep 26, 2024

Found that replacing intersections with unions will break chaning like

      new Hono()
        .get('/bar/:id', c => c.text('foo'))
        .post(c => {
          const id = c.req.param('id') // Woops! `string | undefined`
          return c.text(id)
        })

It's likely there're pitfalls we don't notice so far 🤔

Instead, I'm wondering if we can change the intersection in route method method into union. This will make difference in many projects

): Hono<E, MergeSchemaPath<SubSchema, MergePath<BasePath, SubPath>> & S, BasePath> {

apps added through route are independent each other, I suppose

@m-shaka m-shaka force-pushed the replace-intersection-with-union branch from 60bffbb to b44b0c5 Compare September 28, 2024 07:25
@m-shaka
Copy link
Contributor Author

m-shaka commented Sep 28, 2024

I replaced intersection in route method with union.

I prepared new another script to generate a suitable test case (it's not commited)

import { writeFile } from 'node:fs'
import * as path from 'node:path'

const count = 10
const countSubRoutes = 20

const generateRoutes = (count: number) => {
  let routes = `import { Hono } from '../../../src'
export const app = new Hono()`
  for (let i = 1; i <= count; i++) {
    routes += `
  .route('', new Hono()`
    // each sub app has 20 routes
    for (let j = 1; j <= countSubRoutes; j++) {
        routes += `
    .get('/route${i}-${j}/:id', (c) => {
        return c.json({ ok: true })
    })`
    }
    routes += `
  )`
  }
  return routes
}

const routes = generateRoutes(count)

writeFile(path.join(import.meta.dirname, '../generated/app.ts'), routes, (err) => {
  if (err) { throw err }
  console.log(`${count} routes have been written to app.ts`)
})

Put this into perf-measures/type-check/scripts. Then, you can get a comparison as well

                   Current    Previous
 ---               ---        ---
 Files             248        248
 Lines             113666     113666
 Identifiers       111407     111404
 Symbols           260996     250558
 Types             197294     192000
 Instantiations    1506985    3691507

Instantiaions is down 60%

src/hono-base.ts Outdated Show resolved Hide resolved
@yusukebe
Copy link
Member

yusukebe commented Oct 6, 2024

@m-shaka

I replaced intersection in route method with union.

This is awesome! I also tried it; I could feel it became faster with app.route(). Let's introduce this.

I prepared new another script to generate a suitable test case (it's not committed)

Regarding measuring performance, we can measure two patterns:

  • Handlers without app.route()
  • Hanlders with app.route()

We can choose only the latter to measure it, but in my opinion, it's better to know both performances because there are cases if a user uses app.route() or not. What do you think of this?

If so, the script and CI process will be a bit complicated. But, for measuring app.route(), you can only add this script with using the current app.ts generated:

const count = 5

const generateRoutes = (count: number) => {
  let routes = `import { Hono } from '../../../src'
import { app as subApp } from './app'
export const app = new Hono()`
  for (let i = 1; i <= count; i++) {
    routes += `
  .route('/${i}', subApp)`
  }
  return routes
}

const routes = generateRoutes(count)
// ...

And create two files:

  • generated/app.ts
  • generated/app-with-route.ts

Then measure this tsc for each file. Or there may be any good ideas.

@m-shaka
Copy link
Contributor Author

m-shaka commented Oct 11, 2024

@yusukebe Apologies for my long absence.

Regarding the script for the performance measure, how about doing it in another PR?
I like your idea, but it seems like we need to think about how the display of the result should look like.
We'll have two patterns of perf-measure, so the simple table view may not be suitable

you can only add this script with using the current app.ts generated

#3491 is amazing! After merging it and this PR, I'll improve the display of the result

@m-shaka m-shaka marked this pull request as ready for review October 11, 2024 00:53
@yusukebe
Copy link
Member

@m-shaka

Regarding the script for the performance measure, how about doing it in another PR?

Agree!

I like your idea, but it seems like we need to think about how the display of the result should look like.
We'll have two patterns of perf-measure, so the simple table view may not be suitable

I'm also thinking about this.

After merging it and this PR, I'll improve the display of the result

Yes!


It looks good for me. Can I merge it?

@m-shaka
Copy link
Contributor Author

m-shaka commented Oct 11, 2024

Go!

@yusukebe
Copy link
Member

@m-shaka

Okay! I just released v4.6.4, so let's release it in v4.6.5.

@yusukebe yusukebe merged commit f1a7267 into honojs:main Oct 11, 2024
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants